package mobilefeel;

import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;

/**
 * The main class of the MobileFeel SDK desktop API, instantiate it to access
 * the API. It allows listening for incoming Android device connections, obtain
 * connected devices, and manage DeviceListeners. 
 * */
public final class MobileFeel {

	private ServerSocket serverSocket;
	private List<AndroidDevice> devices; 
	private List<DeviceListener> deviceListeners;	
	private Thread acceptThread;
	
	/**
	 * Sole default constructor. Provides access to the API.
	 */
	public MobileFeel(){
		this.devices = new ArrayList<AndroidDevice>();
		this.deviceListeners = new ArrayList<DeviceListener>();
	}
	
	/**
	 * Registers a callback to be invoked when an Android device connects,
	 * disconnects or sends custom data (Serilizable objects).
	 * 
	 * @param deviceListener the {@link DeviceListener} object that will be
	 * notified of device events (cannot be null)
	 * @see #removeDeviceListener(DeviceListener)
	 */
	public void addDeviceListener(DeviceListener deviceListener){
		if (deviceListener == null)
			throw new IllegalArgumentException("deviceListener cannot be null.");
		this.deviceListeners.add(deviceListener);
	}

	/**
	 * Unregisters a device events listener.
	 * 
	 * @param deviceListener the {@link DeviceListener} object that will not be
	 * notified of device events anymore
	 * @see #addDeviceListener(DeviceListener)
	 */
	public void removeDeviceListener(DeviceListener deviceListener){
		this.deviceListeners.remove(deviceListener);
	}
	
	/**
	 * Starts listening for incoming Android device connections on the
	 * specified port.<br/><br/>
	 * The maximum queue length for incoming connection indications (a request
	 * to connect) is set to 50. If a connection indication arrives when the
	 * queue is full, the connection is refused. 
	 * 
	 * @param port the port number to listen for connection requests on
	 * @throws IOException if an error occurs while opening the connection
	 */
	public void listen(int port) throws IOException{
		this.serverSocket = new ServerSocket(port);
		this.acceptClients();
	}
	
	/**
	 * Starts listening for incoming Android device connections on the
	 * specified port and with the selected backlog.<br/><br/>
	 * The maximum queue length for incoming connection indications (a request
	 * to connect) is set to the backlog parameter. If a connection indication
	 * arrives when the queue is full, the connection is refused.<br/>
	 * The backlog argument must be a positive value greater than 0. If the
	 * value passed is equal or less than 0, then the default value will be
	 * assumed. 
	 * 
	 * @param port the port number to listen for connection requests on
	 * @param backlog the number of pending connection requests, before
	 * requests will be rejected
	 * @throws IOException if an error occurs while opening the connection
	 */
	public void listen(int port, int backlog) throws IOException{
		this.serverSocket = new ServerSocket(port, backlog);
		this.acceptClients();
	}

	/**
	 * Starts listening for incoming Android device connections on the
	 * specified port and with the selected backlog and local bind address.
	 * <br/><br/>
	 * The maximum queue length for incoming connection indications (a request
	 * to connect) is set to the backlog parameter. If a connection indication
	 * arrives when the queue is full, the connection is refused.<br/>
	 * The backlog argument must be a positive value greater than 0. If the
	 * value passed is equal or less than 0, then the default value will be
	 * assumed.<br/>
	 * If bindIP is null, it will be bound to the address of the loopback
	 * interface.  
	 * 
	 * @param port the port number to listen for connection requests on
	 * @param backlog the number of pending connection requests, before
	 * requests will be rejected
	 * @param bindIP the local machine address the server will bind to
	 * @throws IOException if an error occurs while opening the connection
	 */
	public void listen(int port, int backlog, String bindIP) throws IOException{
		this.serverSocket = new ServerSocket(port, backlog, InetAddress.getByName(bindIP));
		this.acceptClients();
	}
	
	/**
	 * Starts listening for incoming Android device connections on the
	 * specified port and with the selected backlog, local bind address and 
	 * timeout.
	 * <br/><br/>
	 * The maximum queue length for incoming connection indications (a request
	 * to connect) is set to the backlog parameter. If a connection indication
	 * arrives when the queue is full, the connection is refused.<br/>
	 * The backlog argument must be a positive value greater than 0. If the
	 * value passed is equal or less than 0, then the default value will be
	 * assumed.<br/>
	 * If bindIP is null, it will be bound to the address of the loopback
	 * interface.<br/>
	 * The timeout must be > 0. A timeout of zero is interpreted as an
	 * infinite timeout. 
	 * 
	 * @param port the port number to listen for connection requests on
	 * @param backlog the number of pending connection requests, before
	 * requests will be rejected
	 * @param bindIP the local machine address the server will bind to
	 * @param timeout the desired timeout (in milliseconds)
	 * @throws SocketTimeoutException when the connection times out
	 * @throws IOException if an error occurs while opening the connection
	 */
	public void listen(int port, int backlog, String bindIP, int timeout) throws SocketTimeoutException, IOException{
		this.serverSocket = new ServerSocket(port, backlog, InetAddress.getByName(bindIP));
		this.serverSocket.setSoTimeout(timeout);
		this.acceptClients();
	}
	
	/**
	 * Stops listening for incoming Android device connections. Calling this
	 * method does not close currently open connections with devices.
	 * Call {@link #disconnectAll()} to do so.
	 * 
	 * @throws IOException if an error occurs while closing the connection
	 */
	public void stopListening() throws IOException {
		if (acceptThread != null && serverSocket != null && !serverSocket.isClosed()) {
			acceptThread.interrupt();
			serverSocket.close();
		}
	}
	
	/**
	 * Ends the connection with all connected Android devices. No further
	 * communication with these devices will be possible unless they reconnect.
	 * Calling this method does not stop the server from listening for new
	 * incoming connections. Call {@link #stopListening()} to do so.
	 * 
	 * @throws IOException if an error occurs while closing the connection
	 * @see AndroidDevice#disconnect()
	 */
	public void disconnectAll() throws IOException {
		for (AndroidDevice device : devices) {
			try {
				device.disconnect();
			} catch (NotConnectedException e) {}
		}
	}
	
	private void acceptClients() throws SocketTimeoutException, IOException {
		acceptThread = new Thread() {
			public void run() {
				while (!acceptThread.isInterrupted()) {
					try {
						new AndroidDevice(MobileFeel.this, serverSocket.accept());
					}
					catch (IOException e) {}
				}
			}
		};
		acceptThread.start();
	}

	synchronized void deviceConnected(AndroidDevice androidDevice) {
		this.devices.add(androidDevice);
		for (DeviceListener listener : this.deviceListeners)
			listener.onDeviceConnected(androidDevice);
	}
	
	synchronized void deviceDisconnected(AndroidDevice androidDevice) {
		this.devices.remove(androidDevice);
		for (DeviceListener listener : this.deviceListeners)
			listener.onDeviceDisconnected(androidDevice);
	}
	
	synchronized void receiveCustomData(AndroidDevice androidDevice, String text, Serializable object) {
		for (DeviceListener listener : this.deviceListeners)
			listener.onCustomDataReceived(androidDevice, text, object);
	}

	/**
	 * Returns a list of connected Android devices.
	 * 
	 * @return a list containing an {@link AndroidDevice} instance for each
	 * 		   connected Android device
	 */
	public List<AndroidDevice> getDevices() {
		return new ArrayList<AndroidDevice>(devices);
	}
}
